package com.example.mq1.mqserver.datacenter;

import com.example.mq1.common.BinaryTool;
import com.example.mq1.common.MqException;
import com.example.mq1.mqserver.core.MSGQueue;
import com.example.mq1.mqserver.core.Message;

import java.io.*;
import java.util.*;

public class MessageFileManager {

    public void init() {

    }

    /**
     * 消息统计数据
     */
    public static class Stat {
        //总消息数
        public int totalCount;
        //有效消息数
        public int validCount;
    }

    public String getQueueDirPath(String queueName) {
        return "./data/" + queueName;
    }

    public String getQueueDataPath(String queueName) {
        return getQueueDirPath(queueName) + "/queue_data.txt";
    }

    public String getQueueStatPath(String queueName) {
        return getQueueDirPath(queueName) + "/queue_stat.txt";
    }

    public Stat readStat(String queueName) {
        Stat stat = new Stat();
        try (InputStream inputStream = new FileInputStream(getQueueStatPath(queueName))) {
            Scanner scanner = new Scanner(inputStream);
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();
            return stat;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public void writeStat(Stat stat, String queueName) {
        try (OutputStream outputStream = new FileOutputStream(getQueueStatPath(queueName))) {
            PrintWriter writer = new PrintWriter(outputStream);
            writer.write(stat.totalCount + "\t" + stat.validCount);
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 创建队列相关文件
     *
     * @param queueName
     * @throws IOException
     */
    public void createQueueFiles(String queueName) throws IOException {
        //1.先检查目录是否存在
        File dirFile = new File(getQueueDirPath(queueName));
        boolean ok = false;
        if (!dirFile.exists()) {
            ok = dirFile.mkdirs();
            if (!ok) {
                throw new IOException("[MessageFileManager] 消息目录创建失败！ path=" + getQueueDirPath(queueName));
            }
        }
        //2.检查数据文件是否存在，不存在就创建
        File dataFile = new File(getQueueDataPath(queueName));
        if (!dataFile.exists()) {
            ok = dataFile.createNewFile();
            if (!ok) {
                throw new IOException("[MessageFileManager] 消息数据文件创建失败！ path=" + getQueueDirPath(queueName));
            }
        }
        //3.检查统计文件是否存在，不存在就创建
        File statFile = new File(getQueueStatPath(queueName));
        if (!statFile.exists()) {
            ok = statFile.createNewFile();
            if (!ok) {
                throw new IOException("[MessageFileManager] 消息统计文件创建失败！ path=" + getQueueDirPath(queueName));
            }
        }
        //4.初始化统计信息
        Stat stat = new Stat();
        stat.totalCount = 0;
        stat.validCount = 0;
        writeStat(stat, queueName);
    }

    /**
     * 删除队列相关文件
     *
     * @param queueName
     */
    public void destroyQueueFile(String queueName) throws IOException {
        //先删除文件，再删除目录
        File dataFile = new File(getQueueDataPath(queueName));
        boolean ok1 = dataFile.delete();
        File statFile = new File(getQueueStatPath(queueName));
        boolean ok2 = statFile.delete();
        File dirFile = new File(getQueueDirPath(queueName));
        boolean ok3 = dirFile.delete();
        if (!ok1 || !ok2 || !ok3) {
            throw new IOException("[MessageFileManager] 队列文件删除失败！ path=" + getQueueDirPath(queueName));
        }
    }

    /**
     * 检查队列文件是否存在
     *
     * @param queueName
     * @return
     */
    public boolean checkFilesExists(String queueName) {
        File statFile = new File(getQueueStatPath(queueName));
        boolean ok2 = statFile.exists();
        File dataFile = new File(getQueueDataPath(queueName));
        boolean ok3 = dataFile.exists();
        return ok2 && ok3;
    }

    /**
     * 将发送的新消息存储到队列文件中
     *
     * @param queue
     * @param message
     */
    public void sendMessage(MSGQueue queue, Message message) throws MqException, IOException {
        if (!checkFilesExists(queue.getQueueName())) {
            throw new MqException("[MessageFileManager] 队列相关文件不存在！ path=" + getQueueDirPath(queue.getQueueName()));
        }
        //消息序列化成二进制的
        byte[] messageByte = BinaryTool.toBytes(message);
        synchronized (queue) {
            File dataFile = new File(getQueueDataPath(queue.getQueueName()));
            message.setOffsetBeg(dataFile.length() + 4);
            message.setOffsetEnd(dataFile.length() + 4 + messageByte.length);
            try (OutputStream outputStream = new FileOutputStream(dataFile, true)) {//这里 true 表示追加到文件末尾
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    dataOutputStream.writeInt(messageByte.length);
                    dataOutputStream.write(messageByte);
                    dataOutputStream.flush();
                }
            }
        }
        //更新消息统计信息
        Stat stat = readStat(queue.getQueueName());
        stat.validCount += 1;
        stat.totalCount += 1;
        writeStat(stat, queue.getQueueName());
        System.out.println("[MessageFileManager] 消息成功写入队列文件，并更新统计信息！ queueName=" + queue.getQueueName());
    }

    /**
     * 删除消息文件(逻辑删除)
     *
     * @param queue
     * @param message
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public void deleteMessage(MSGQueue queue, Message message) throws IOException, ClassNotFoundException {
        synchronized (queue) {
            //1.将对应消息从队列文件中读出来，序列化成对象
            byte[] messageByte = new byte[(int) (message.getOffsetEnd() - message.getOffsetBeg())];
            try (RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataPath(queue.getQueueName()), "rw")) {
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.read(messageByte);
                Message messageFile = (Message) BinaryTool.fromBytes(messageByte);
                //2.将 isValid 改为 0
                messageFile.setIsValid((byte) 0x0);
                //3.将文件写入队列文件对应的位置
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.write(BinaryTool.toBytes(messageFile));
            }
            //4.更新消息统计信息
            Stat stat = readStat(queue.getQueueName());
            if (stat.validCount > 0) {
                stat.validCount -= 1;
            }
            writeStat(stat, queue.getQueueName());
            System.out.println("[MessageFileManager] 消息文件删除成功(逻辑删除)，并更新统计文件！ queueName=" + queue.getQueueName());
        }
    }

    /**
     * 从队列文件中读出所有有效的消息
     *
     * @return
     */
    public LinkedList<Message> loadAllMessageFromQueue(String queueName) throws IOException, ClassNotFoundException, MqException {
        LinkedList<Message> messages = new LinkedList<>();
        try (InputStream inputStream = new FileInputStream(getQueueDataPath(queueName))) {
            try (DataInputStream dataInputStream = new DataInputStream(inputStream)) {
                //记录当前读取到文件的哪一个位置
                long curFileIndex = 0;
                while (true) {
                    //读出消息
                    int messageLen = dataInputStream.readInt();
                    byte[] messageByte = new byte[messageLen];
                    //读出消息实际长度
                    int actualLen = dataInputStream.read(messageByte);
                    //判断理论长度和实际长度是否符合
                    if(messageLen != actualLen) {
                        throw new MqException("[MessageFileManager] 读出理论实际消息长度和实际长度不符！ expected=" + messageLen +
                                ", actual=" + actualLen);
                    }
                    //序列化成消息对象,过滤掉无效消息
                    Message message = (Message) BinaryTool.fromBytes(messageByte);
                    if (message.getIsValid() != 0x1) {
                        curFileIndex = curFileIndex + 4 + messageLen;
                        continue;
                    }
                    //更新offset
                    message.setOffsetBeg(curFileIndex + 4);
                    message.setOffsetEnd(curFileIndex + 4 + messageLen);
                    curFileIndex = curFileIndex + 4 + messageLen;
                    //将有效信息加入到 List 中
                    messages.add(message);
                }
            } catch (EOFException e) {
                System.out.println("[MessageFileManager] 队列消息读取到内存完成！");
            }
        }
        return messages;
    }

    /**
     * 检测该队列文件是否进行 GC
     * @param queueName
     * @return
     */
    public boolean checkGC(String queueName) {
        Stat stat = readStat(queueName);
        if (stat.totalCount > 2000 && ((double) stat.validCount / (double) stat.totalCount < 0.5)) {
            return true;
        }
        return false;
    }

    /**
     * 获取新文件目录新文件
     * @param queueName
     * @return
     */
    public String getNewDataFilePath(String queueName) {
        return getQueueDirPath(queueName) + "/queue_data_new.txt";
    }

    /**
     * 进行真正的 GC
     * 这里使用复制算法
     * 先读出旧队列数据文件中所有有效数据，然后创建队列数据新文件，将旧文件中所有有效的数据读出来输入到新文件中，
     * 然后删除旧文件，将新文件改名为旧文件，最后更新统计文件
     * @param queue
     * @throws IOException
     * @throws ClassNotFoundException
     * @throws MqException
     */
    public void gc(MSGQueue queue) throws IOException, ClassNotFoundException, MqException {
        synchronized (queue) {
            //GC 可能会比较耗时，因此记录一下时间
            long startTime = System.currentTimeMillis();
            //1.将旧文件中所有有效数据都获取到
            File oldDataFile = new File(getQueueDataPath(queue.getQueueName()));
            List<Message> messages = loadAllMessageFromQueue(queue.getQueueName());
            //2.创建新文件，并将刚刚获取到的信息都写入
            File newDataFile = new File(getNewDataFilePath(queue.getQueueName()));
            if(newDataFile.exists()) {
                throw new MqException("[MessageFileManager] GC 时创建新文件前发现新文件已经存在！ newDataFile=" + newDataFile.getAbsoluteFile());
            }
            boolean ok = newDataFile.createNewFile();
            if(!ok) {
                throw new MqException("[MessageFileManager] GC 时创建新队列数据文件失败！ newDataFile=" + newDataFile.getAbsoluteFile());
            }
            try (OutputStream outputStream = new FileOutputStream(newDataFile, true)) {
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    for(Message message : messages) {
                        byte[] messageByte = BinaryTool.toBytes(message);
                        dataOutputStream.writeInt(messageByte.length);
                        dataOutputStream.write(messageByte);
                        dataOutputStream.flush();
                    }
                }
            }
            //3.删除旧文件
            ok = oldDataFile.delete();
            if(!ok) {
                throw new MqException("[MessageFileManager] GC 时删除旧队列数据文件失败！ oldDataFile=" + oldDataFile.getAbsoluteFile());
            }
            //4.将新文件名改名为旧文件名
            ok = newDataFile.renameTo(oldDataFile);
            if(!ok) {
                throw new MqException("[MessageFileManager] GC 时新文件更名为旧文件失败失败！ oldDataFile=" + oldDataFile.getAbsoluteFile() +
                        ", newDataFile=" + newDataFile.getAbsoluteFile());
            }
            //5.更新统计文件
            Stat stat = readStat(queue.getQueueName());
            stat.totalCount = messages.size();
            stat.validCount = messages.size();
            writeStat(stat, queue.getQueueName());
            long endTime = System.currentTimeMillis();
            System.out.println("[MessageFileManager] GC 完成！queueName=" + queue.getQueueName() + ", time=" + (endTime - startTime) + "ms");

        }
    }


}